import inspect

class NoDefault(object):
    def __repr__(self):
        return "NoDefault"

class ChainError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)    

PREFIX_ARGS = 1

def getargs(f):
    spec = inspect.getargspec(f)    
    args = [ x for x in spec[0][PREFIX_ARGS:] ]
    vals = [ x for x in spec[3] ] if spec[3] else []
    diff = len(args) - len(vals)

    args_spec = dict([ (args[i], vals[i - diff] if i - diff >= 0 else NoDefault()) 
                       for i in range(len(args)) ])
    return (args_spec, args, diff)


def Chained(method=None, path_part=None):

    def wrap(f):
        (spec, all_args, req_args_nr) = getargs(f)
        req_args = all_args[0:req_args_nr]
        args     = req_args_nr

        f._spec = spec
        f._req_args = req_args

        _path_part = path_part if path_part != None else f.__name__
        method_capture = strip_slash(method._capture_path) if method else ''
        capture_path   = method_capture + ('/' if len(_path_part) > 0 else '') + _path_part

        # generate url pattern - with the defaults set as in the specifications
        for arg in all_args:
            capture_path += (r'/(?P<%s>[^/]+)', r'(?:/(?P<%s>[^/]+))?')[type(spec[arg]) != NoDefault] % (arg)

        capture_path = strip_slash(capture_path)
        f._capture_path = capture_path 

        # _all_spec contains all chained parameters up until this point
        #   this method's parameters + the chained method's _all_spec
        f._all_spec = f._spec
        method_spec = method._all_spec if method else {}
        for k in method_spec.keys():
            if f._all_spec.has_key(k):
                raise ChainError("Duplicate parameter in chained method '%s': %s" % (f.__name__, k))
            f._all_spec[k] = method_spec[k]
 
        f._all_req_args = f._req_args + (method._all_req_args if method else [])

        def wrapped_f(*a, **kwa):
            prefix_a = list(a[0:PREFIX_ARGS])
            a        = list(a[PREFIX_ARGS:])

            if method:
                req_args = method._all_req_args
                spec     = method._all_spec
                nr_args  = len(method._all_req_args)

                chain_kwa = {}
                for k in kwa.keys():
                    if k in spec:
                        chain_kwa[k] = kwa.pop(k)
                        if k in req_args:
                            nr_args -= 1

                result = method( *(prefix_a + a[0:nr_args]), **chain_kwa ) 
                if result:
                    return result
                a = a[nr_args:]

            return f( *(prefix_a + a), **kwa ) 

        wrapped_f.__name__ = f.__name__
        wrapped_f.__dict__ = f.__dict__
        wrapped_f.__doc__  = f.__doc__
        return wrapped_f

    return wrap


def strip_slash(s):
    if not len(s): return ''

    new_s = s
    if new_s[-1:] == '/': # eliminate trailing slash
        new_s = new_s[0:-1]
    if len(new_s) and new_s[0] == '/': # eliminate leading slash
        new_s = new_s[1:]        
    return new_s
